<%#
kind: provision
name: Junos default SLAX
model: ProvisioningTemplate
oses:
- Junos
description: |
  Provisioning template for the Junos OS, used on Juniper network devices.
  It fetches the config file based on the Juniper Finish template and deploys
  it if it differs from the current version.
-%>
version 1.0;

/* ------------------------------------------------------------------
 * LICENSE
 * ------------------------------------------------------------------
 *
 * Copyright (c) 2013, Juniper Networks
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 *
 * (1) Redistributions of source code must retain the above copyright notice, this 
 * list of conditions and the following disclaimer. 
 *
 * (2) Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution. 
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of Juniper Networks.
 * 
 *
 * ------------------------------------------------------------------
 * AUTHORS AND CONTRIBUTORS
 * ------------------------------------------------------------------
 *
 * Jeremy Schulman, Juniper Networks
 * - initial release (https://github.com/jeremyschulman/jctyztp/)
 *
 * Frank Wall, noris network AG
 * - adapt for integration in The Foreman
 * - fix compatibility with older Junos releases
 * 
 *
 * ------------------------------------------------------------------
 * LIMITATIONS
 * ------------------------------------------------------------------
 *
 * To maintain backwards compatibility with JunOS 11.x (and maybe 
 * even 10.x) you MUST AVOID all of these:
 * - global variable $junos-context (introduced with Junos 11.1)
 * - mutable variables (introduced with JunOS 12.2 -> SLAX 1.1)
 * - native functions (introduced with JunOS 12.2 -> SLAX 1.1)
 *
 * ------------------------------------------------------------------
 */

/* ------------------------------------------------------------------ */
/* XML namespaces                                                     */
/* ------------------------------------------------------------------ */

/* Juniper */
ns junos = "http://xml.juniper.net/junos/*/junos";
ns xnm = "http://xml.juniper.net/xnm/1.1/xnm";
ns jcs = "http://xml.juniper.net/junos/commit-scripts/1.0";
ns ztp = "http://xml.juniper.net/junos/ztp";

/* EXSLT */
ns exsl extension = "http://exslt.org/common";
ns func extension = "http://exslt.org/functions";
ns str extension = "http://exslt.org/strings";

/* private namespace for this script */
ns jctyztp = "http://xml.juniper.com/jctyztp/1.0";
ns fmztp = "http://xml.juniper.com/fmztp/1.0";

/* depending on Junos version relative path may be broken */
/* import '../import/junos.xsl'; */
import '/usr/libdata/cscript/import/junos.xsl';

/* ------------------------------------------------------------------ */
/* Script parameters                                                  */
/* ------------------------------------------------------------------ */

param $server = 'ztpserver';
param $mediapath = '<%= @mediapath %>';

/* ------------------------------------------------------------------ */
/* Constants                                                          */
/* ------------------------------------------------------------------ */

var $APPNAME = 'foreman-ztp';
var $SYSLOG = 'user.info';
var $TMPDIR = '/var/tmp';
var $JUNOS_CONF = '/var/tmp/junos.conf';

var $ZTP_GROUP_NAME = "fmztp";
var $ZTP_MACRO_NAME = "conf";
var $ZTP_LOCKFILE = '/tmp/fmztp.lock';

/* ------------------------------------------------------------------ */
/* Global variables                                                   */
/* ------------------------------------------------------------------ */

/* Open a connection to the device API */
var $jnx = jcs:open();

/* ------------------------------------------------------------------ */
/*                                MAIN                                */ 
/* ------------------------------------------------------------------ */

match / {

  /* Terminate on connection error */
  if(not( $jnx )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to connect to Junos API");
    expr fmztp:terminate();
  }
  
  var $running = fmztp:only_once();
  if( $running ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": start op script: already running, exiting..." );
    <xsl:message terminate="yes">;
  }
  
  /* if the $JUNOS_CONF file is not on the device, then */
  /* download it from the server */
  if(not( fmztp:file-exists( $JUNOS_CONF ))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": obtaining device config file");
    var $cp = fmztp:dl_junos_conf();
  }
  
  /* now load $JUNOS_CONF into the candidate configuration so we can */
  /* extract the ZTP config */
  var $ztp_conf = fmztp:ld_junos_conf();
  var $has_version = $ztp_conf/has_version;
  var $new_package = $ztp_conf/package;
  expr jcs:syslog( $SYSLOG, $APPNAME, ": preparing update from ", $has_version, " to ", $new_package );
  
  /* if we have a version difference, then we will install the new OS */
  /* and reboot the device.  the $JUNOS_CONF file will be loaded on */
  /* after the install process completes */
  if( $ztp_conf/install ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": Junos install required" );
    var $os = fmztp:install_os( $ztp_conf );
    expr jcs:syslog( $SYSLOG, $APPNAME, ": rebooting in 60 seconds" );
    expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-END");
    expr fmztp:reboot( 1 );
    expr jcs:close( $jnx );
  }
  else {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": no Junos install required" );
    /* puppet agent is only supported on Junos 12.3R2.5 */
    if( jcs:regex( "12.3R2.5", $ztp_conf/has_version )) {
	expr jcs:syslog( $SYSLOG, $APPNAME, ": puppet agent install required" );
	var $puppet = fmztp:install_puppet( $ztp_conf );
    }
    else {
	expr jcs:syslog( $SYSLOG, $APPNAME, ": os version is ", $has_version, ", but puppet agent is only available on 12.3R2.5" );
    }
    var $fini = fmztp:finalize();
    expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-END");
    expr jcs:close( $jnx );
  }
}

/* ------------------------------------------------------------------ */
/* HTTP Junos configuration file onto the device                      */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:dl_junos_conf">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading new Junos conf...");
  expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", '<%= foreman_url('finish') %>');

  var $get = <file-copy> {
    <source> '<%= foreman_url('finish') %>';
    <destination> $JUNOS_CONF;
    <staging-directory> $TMPDIR;
  };

  var $got = jcs:execute( $jnx, $get );

  if(not( fmztp:file-exists( $JUNOS_CONF ))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to find new Junos conf at ", $JUNOS_CONF);
    expr fmztp:terminate();
  }

  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Load the $JUNOS_CONF file into the candidate config and extract    */
/* the ZTP config from the [edit groups] area.  Do *NOT* commit       */
/* this configuration yet, since we may need to install the OS first  */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:ld_junos_conf">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": loading new Junos conf...");

  /* get the current version from the configuration file */
  
  var $get_cur_ver = <get-configuration database='committed'> { <configuration> { <version>; }};
  var $got_cur_ver = jcs:execute( $jnx, $get_cur_ver );

  /* now load the configuration file we got from the ztp server */
  var $do_load = <load-configuration action="override" url=$JUNOS_CONF format="text">;
  var $did_load = jcs:execute( $jnx, $do_load );
  if(not( $did_load/load-success )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to load config ", $JUNOS_CONF );
    expr fmztp:terminate();
  }
  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": extracting Junos config parameters...");
  var $get = <get-configuration> { <configuration> {
    <version>;
    <groups> { <name> $ZTP_GROUP_NAME;
      <apply-macro> { <name> $ZTP_MACRO_NAME;
      }
    }
  }};
  
  var $got = jcs:execute( $jnx, $get );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": package: ", $got//data[name = 'package']/value);
  expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $mediapath);

  /* create a node-set to store the following elements             */
  /* has_version = current Junos version string                    */
  /* package = filename of Junos package (*.tgz)                   */
  /* mediapath = URL where package is obtained from                */
  /* install = present if a install is requeired                   */
  var $ver = $got_cur_ver/version;
  var $package = $got//data[name = 'package']/value;
  var $puppet = $got//data[name = 'puppet']/value;
  var $conf := {
    <has_version> $ver; 
    <package> $package;
    <puppet> $puppet;
    <url> $mediapath;
    if(not( jcs:regex( $ver, $package ))) {
      <install>;
    }
  }

  /* @@@ should put some trap here on ensuring that the config */
  /* @@@ file actually had the correct group/macro defined */
    
  <func:result select="$conf">;
}

/* ------------------------------------------------------------------ */
/* Junos Software Installation - download the software from the HTTP  */
/* server and perform the 'request system software add' operation     */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:install_os">
{
  param $ztp_conf;

  var $local_image = $TMPDIR _ "/" _ $ztp_conf/package;
  
  if( fmztp:file-exists( $local_image )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": junos image exists, no download needed" );
  }
  else {
  
    /* request system storage cleanup */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": cleaning filesystem" );
    var $clean = jcs:execute( $jnx, 'request-system-storage-cleanup' ); 

    /* file copy .... */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading junos image..." );    
    expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $ztp_conf/url, $ztp_conf/package );
    var $do_copy = <file-copy> {
      <source> $ztp_conf/url _ $ztp_conf/package;
      <destination> $TMPDIR;
      <staging-directory> $TMPDIR;
    };
    var $did_copy = jcs:execute( $jnx, $do_copy );
    
    /* trap error here */
    if( not(fmztp:file-exists( $local_image )) ) {
        expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: unable to download junos image" );
        expr fmztp:terminate();
    }
  }
  
  /* request system software add ... */
  expr jcs:syslog( $SYSLOG, $APPNAME, ": installing junos image" );    
  var $do_install = <request-package-add> {
    <no-validate>;
    <package-name> $local_image;
  };
  var $did_install = jcs:execute( $jnx, $do_install );
  /* @@@ need to trap error here on $did_install */
  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": completed installing junos image" );
   
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Reboot the device given a delay, in minutes                        */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:reboot">
{
  param $in_min;

  var $do_reboot = <request-reboot> { <in> $in_min; };
  var $did_reboot = jcs:execute( $jnx, $do_reboot );
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Puppet Agent Installation - download the software from the HTTP    */
/* server and perform the 'request system software add' operation     */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:install_puppet">
{
  param $ztp_conf;
  expr jcs:syslog( $SYSLOG, $APPNAME, ": starting puppet installation..." );

  if (fmztp:is-installed("puppet")) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package puppet is already installed" );
    <func:result select="true()">;
  }

  var $local_image = $TMPDIR _ "/" _ $ztp_conf/puppet;
  
  if( fmztp:file-exists( $local_image )) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package puppet exists, no download needed" );
  }
  else {
  
    /* request system storage cleanup */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": cleaning filesystem" );
    var $clean = jcs:execute( $jnx, 'request-system-storage-cleanup' ); 

    /* file copy .... */
    expr jcs:syslog( $SYSLOG, $APPNAME, ": downloading puppet image..." );    
    expr jcs:syslog( $SYSLOG, $APPNAME, ": URL: ", $ztp_conf/url, $ztp_conf/puppet );    
    var $do_copy = <file-copy> {
      <source> $ztp_conf/url _ $ztp_conf/puppet;
      <destination> $TMPDIR;
      <staging-directory> $TMPDIR;
    };
    var $did_copy = jcs:execute( $jnx, $do_copy );
    
    /* trap error on $did_copy */
    if( not(fmztp:file-exists( $local_image )) ) {
      expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: failed to download puppet image" );
      expr fmztp:terminate();
    }
  }

  /* request system software add ... */
  expr jcs:syslog( $SYSLOG, $APPNAME, ": installing puppet image" );    
  var $do_install = <request-package-add> {
    <no-validate>;
    <package-name> $local_image;
  };
  var $did_install = jcs:execute( $jnx, $do_install );
  
  /* NOTE: To complete the puppet installation you need to take manual steps, see:
   *       http://www.juniper.net/techpubs/en_US/release-independent/junos-puppet/information-products/pathway-pages/index.html
   */

  /* validate installation */
  if (not(fmztp:is-installed("puppet"))) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ": ERROR: failed to install package puppet" );
    expr fmztp:terminate();
  }

  expr jcs:syslog( $SYSLOG, $APPNAME, ": completed installing puppet image" );
   
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Finalize the ZTP process; i.e. after the OS is correct.  Remove    */
/* the $JUNOS_CONF file and committing the configuration to make      */
/* it active.                                                         */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:finalize">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": deleting temp config file...");  
  var $rm1 = fmztp:file-delete( $JUNOS_CONF );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": sending finish signal to foreman...");  
  var $do_foreman = <file-copy> {
      <source> '<%= foreman_url('provision') %>';
      <destination> "/tmp";
      <staging-directory> "/tmp";
  };
  var $did_foreman = jcs:execute( $jnx, $do_foreman );

  expr jcs:syslog( $SYSLOG, $APPNAME, ": deleting ztp lock file...");  
  var $rm2 = fmztp:file-delete( $ZTP_LOCKFILE );

  /* commit the configuration that was previously loaded */  
  expr jcs:syslog( $SYSLOG, $APPNAME, ": committing configuration...");  
  var $commit = jcs:execute( $jnx, 'commit-configuration' );
  if( $commit//self::xnm:error ) {
    expr jcs:syslog( $SYSLOG, $APPNAME, ":ERROR: unable to commit configuration: ", $commit//self::xnm:error/message );
    var $die = fmztp:terminate();
  }

  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Helper routine: check to see if a file exists on the device,       */
/* returns [ true | false ]                                           */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:file-exists">
{
  param $filename;
  var $ls_file = <file-list> { <path> $filename; };
  var $ls_got = jcs:execute( $jnx, $ls_file );
  var $retval = boolean( $ls_got//file-information );

  <func:result select="$retval">;
}

<func:function name="fmztp:file-delete">
{
  param $filename;
  var $do_rm = <file-delete> { <path> $filename; };
  var $did_rm = jcs:execute( $jnx, $do_rm );
  /* @@@ trap error */
  
  <func:result select="true()">;
}

/* ------------------------------------------------------------------ */
/* Helper routine: create a lockfile to make sure the script only     */
/* runs once, and terminate if it is already running.                 */
/* returns [ true | false ]                                           */
/* ------------------------------------------------------------------ */

<func:function name="fmztp:only_once">
{
  if( fmztp:file-exists( $ZTP_LOCKFILE )) {
    <func:result select="true()">;
  }
  else {
    var $do_lock = <file-put> {
      <filename> $ZTP_LOCKFILE;
      <encoding> 'ascii';
      <file-contents> 'locked';
    };
    var $did_lock = jcs:execute( $jnx, $do_lock );
    <func:result select="false()">;
  }
}

<func:function name="fmztp:terminate">
{
  expr jcs:syslog( $SYSLOG, $APPNAME, ": SCRIPT-FAILED" );
  var $rm_lock = fmztp:file-delete( $ZTP_LOCKFILE );
  <xsl:message terminate="yes">;
}

/* ------------------------------------------------- */
/* check if software package is installed            */
/* ------------------------------------------------- */
<func:function name="fmztp:is-installed">
{
  param $string;

  var $do_query = <get-software-information>;
  var $did_query = jcs:execute( $jnx, $do_query );

  if( jcs:regex( $string, $did_query )) {
    expr jcs:output("package found: ", $string);
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package found: ", $string);
    <func:result select="true()">;
  }
  else {
    expr jcs:output("package NOT found: ", $string);
    expr jcs:syslog( $SYSLOG, $APPNAME, ": package NOT found: ", $string);
    <func:result select="false()">;
  }
}
